/*
 * Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package google_io2011_logad;

import com.google.api.client.auth.oauth2.draft10.AccessTokenErrorResponse;
import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;
import com.google.api.client.googleapis.auth.oauth2.draft10.GoogleAccessTokenRequest.GoogleAuthorizationCodeGrant;
import com.google.api.client.googleapis.auth.oauth2.draft10.GoogleAuthorizationRequestUrl;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;

import java.awt.Desktop;
import java.awt.Desktop.Action;
import java.io.IOException;
import java.net.URI;
import java.util.logging.Logger;
import java.util.Scanner;

/**
 * A helper for doing OAuth 2.0 authentication of a "native" application.
 *
 * A "native" flow recommended for installed clients in which the end user must
 * grant access in a web browser and then copy a code into the application.
 *
 * Usage:
 *   // Obtain the secrets needed to identity this application
 *   OAuth2ClientIdentity clientIdentity = new OAuth2ClientIdentity(...);
 *   // Instantiate this helper to authorize the client for a particular set
 *   // of scopes.
 *   OAuth2Native authFlow = new OAuth2Native(
 *      clientIdentity, "https://www.googleapis.com/auth/buzz");
 *
 *   // Do the authorization dance and get an access token
 *   String token = authFlow.getAccessToken();
 *
 *   // Have each request on a transport use the access token.
 *   AccessProtectedResource.usingAuthorizationHeader(transport, token);
 *
 * @author Tony Aiuto (aiuto@google.com)
 */
class OAuth2NativeFlow {

  private static final Logger logger = Logger.getLogger("OAuth2NativeFlow");

  /**
   * The application credentials and requested scopes.
   */
  OAuth2ClientIdentity clientIdentity;

  /**
   * The scopes you want the user to grant access to.
   */
  final String scopes;

  /**
   * The URL to reach the local server we have started.
   */
  private String localServerUrl;

  public OAuth2NativeFlow(OAuth2ClientIdentity clientIdentity, String scopes) {
    this.clientIdentity = clientIdentity;
    this.scopes = scopes;
  }

  /**
   * Get an access token authorizing this application to use the given scope
   * on behalf of the user.
   *
   * @return the token.
   */
  public String getAccessToken() throws Exception {
    // Get the URL the user must visit to see the grant request from the OAuth
    // server for Google services.
    String authorizationUrl = new GoogleAuthorizationRequestUrl(
        clientIdentity.clientId,
        clientIdentity.redirectUris,
        scopes).build();

    Log("OAuth: Built the URL for an OAuth request: \n    " + authorizationUrl);
    Log("OAuth: The user must visit that URL in their browser.");
    launchInBrowser(authorizationUrl);

    // Get the access code from the authorization page and turn it into a token
    String token = null;
    while (token == null) {
      // request code from user
      String code = "";
      while (code.isEmpty()) {
        System.out.print("Please enter code: ");
        code = new Scanner(System.in).nextLine();
      }
      Log("OAuth: Asking the OAuth server to turn the verification code\n"
          + "into an access token...");
      token = exchangeCodeForAccessToken(code);
      Log("OAuth: Got the access token: " + token);
    }
    return token;
  }

  /**
   * Turn the access code returned from an OAuth request into a token for
   * branding into each HTTP request to the server.
   *
   * @param code The access code from the user's approval.
   * @return The access token to put in the HTTP request headers.
   */
  private String exchangeCodeForAccessToken(String code) throws IOException {
    try {
      // exchange code for an access token
      GoogleAuthorizationCodeGrant request =
        new GoogleAuthorizationCodeGrant(new NetHttpTransport(),
            new JacksonFactory(),
            clientIdentity.clientId,
            clientIdentity.clientSecret,
            code,
            clientIdentity.redirectUris);
      AccessTokenResponse response = request.execute();
      return response.accessToken;
    } catch (HttpResponseException e) {
      AccessTokenErrorResponse response = e.response.parseAs(AccessTokenErrorResponse.class);
      System.out.println();
      System.err.println("Error: " + response.error);
      System.out.println();
    }
    return null;
  }

  /**
   * Launch a browser to display a URL.
   *
   * If Desktop.BROWSE is not supported, it falls back to printing a URL on
   * the console.
   *
   * @param url The URL to display.
   */
  private static void launchInBrowser(String url) {
    // launch in browser
    boolean browsed = false;
    if (Desktop.isDesktopSupported()) {
      Desktop desktop = Desktop.getDesktop();
      if (desktop.isSupported(Action.BROWSE)) {
        try {
          desktop.browse(URI.create(url));
          browsed = true;
        } catch (Exception e) {
          System.out.println();
          System.err.println("Error: " + e.getMessage());
          System.out.println();
        }
      }
    }
    // Fallback to running Chrome.  Works only in Linux
    if (!browsed) {
      try {
        String browser = "google-chrome";
        Runtime.getRuntime().exec(new String[] {browser, url});
        browsed = true;
      } catch (IOException e) {
        // ignore and fall through
      }
    }
    // Fallback to command line
    if (!browsed) {
      System.out.println("Open the following URL in your favorite browser:");
      System.out.println("  " + url);
    }
  }

  private static void Log(String s) {
    System.out.println("");
    System.out.println(s);
  }
}
